﻿using System;
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using NUnit.Framework;
using ReactiveGraph.Core;
using ReactiveGraph.Core.Extensions;
using ReactiveGraph.Tests.Containers;
using ReactiveGraph.Tests.Extensions;

namespace ReactiveGraph.Tests
{
    [TestFixture]
    public class ReactiveNodeTests
    {
        private DefaultTestContainer _container;

        [SetUp]
        public void Setup()
        {
            _container = new DefaultTestContainer();
        }
        
        [Test]
        public void NodeCanHaveProperties()
        {
            var node = _container.GetNode<ReactiveNode>();
            node.AttachProperty(new Property<int>("Prop1"));
            node.AttachProperty(new Property<string>("Prop2"));

            node.Properties.Count().Should().Be(2);
            var p = node.GetProperty("Prop1");
            p.Name.Should().Be("Prop1");
        }

        [Test]
        public void NodeCanChildNodes()
        {
            var node = _container.GetNode<ReactiveNode>();
            var node1 = _container.GetNode<ReactiveNode>();
            node.ChildNodes.Add(node1);
            var node2 = _container.GetNode<ReactiveNode>();
            node.ChildNodes.Add(node2);

            node.ChildNodes.Count().Should().Be(2);
            var n = node.ChildNodes[node2.NodeName];
            n.Should().BeSameAs(node2);
        }

        [Test]
        public void CanAddCustomProperty()
        {
            var node = _container.GetNode<ReactiveNode>();
            node.AttachProperty(new CustomProperty<string>("Prop1"));
            var p = node.GetProperty("Prop1");
            p.Name.Should().Be("Prop1");
        }

        private class CustomProperty<T> : Property<T>
        {
            public CustomProperty(string name) 
                : base(name)
            {
            }
        }

        [Test]
        public void CanSetPropertyValue()
        {
            var node = _container.GetNode<ReactiveNode>();
            node.AttachProperty(new Property<int>("Prop1"));
            var p = node.GetProperty("Prop1");
            p.Value = 42;
            p.Value.Should().Be(42);
        }

        [Test]
        [ExpectedException(typeof(InvalidCastException))]
        public void SetPropertyToWrongTypeThrows()
        {
            var node = _container.GetNode<ReactiveNode>();
            node.AttachProperty(new Property<int>("Prop1"));
            var p = node.GetProperty("Prop1");
            p.Value = "foo";
        }

        [Test]
        public void NodeNotifiesWhenPropertyChanges()
        {
            var node = _container.GetNode<ReactiveNode>();
            node.AttachProperty(new Property<int>("Prop1"));
            var triggered = new List<NodeEvent>();
            node.Events.Do(triggered.Add).Subscribe();
            var p = node.GetProperty("Prop1");
            p.Value = 42;
            triggered.Should().HaveCount(1);

            var nodeEvent = triggered.First();
            nodeEvent.Type.Should().Be(EventType.PropertyChange);
            nodeEvent.Source.Node.Should().Be(node);
            nodeEvent.Source.Property.Should().Be(p);
            nodeEvent.Source.PropertyTriggerValue.Should().Be(42);

        }

        [Test]
        public void ChildrenNodesArePartOfTheEventChain()
        {
            var node = _container.GetNode<ReactiveNode>();
            var p1 = new Property<int>("Prop1");
            node.AttachProperty(p1);

            var parentNode = _container.GetNode<ReactiveNode>();
            parentNode.ChildNodes.Add(node);

            var events = new List<NodeEvent>();
            parentNode.Events.Do(events.Add).Subscribe();
            p1.Value = 42;

            events.Should().HaveCount(1);
        }

        [Test]
        public void CanDetach()
        {
            var node = _container.GetNode<ReactiveNode>();
            var child   = _container.GetNode<ReactiveNode>();
            node.ChildNodes.Add(child);
            node.ChildNodes.Remove(child);
            node.ChildNodes.Count().Should().Be(0);
        }

        [Test]
        public void Clear()
        {
            var node = _container.GetNode<ReactiveNode>();
            node.ChildNodes.Add(_container.GetNode<ReactiveNode>());
            node.ChildNodes.Add(_container.GetNode<ReactiveNode>());
            node.ChildNodes.Clear();
            node.ChildNodes.Count().Should().Be(0);
        }

        [Test]
        public void NodeAddedEventWhenNodeIsAdded()
        {
            var parent = _container.GetNode<ReactiveNode>();
            
            var triggered = new List<NodeEvent>();
            parent.Events.Do(triggered.Add).Subscribe();
            var n1 = _container.GetNode<ReactiveNode>();
            parent.ChildNodes.Add(n1);

            var nodeEvent = triggered.Last();
            nodeEvent.Type.Should().Be(EventType.DescendantAdded);
            nodeEvent.Source.ParentNode.Should().Be(parent);
            nodeEvent.Source.Node.Should().Be(n1);
        }

        [Test]
        public void NodeRemovedEventWhenNodeIsRemoved()
        {
            var parent = _container.GetNode<ReactiveNode>();

            var triggered = new List<NodeEvent>();
            var n1 = _container.GetNode<ReactiveNode>();
            parent.ChildNodes.Add(n1);
            parent.Events.Do(triggered.Add).Subscribe();
            parent.ChildNodes.Remove(n1);

            var nodeEvent = triggered.Last();
            nodeEvent.Type.Should().Be(EventType.DescendantRemoved);
            nodeEvent.Source.ParentNode.Should().Be(parent);
            nodeEvent.Source.Node.Should().Be(n1);
        }

        [Test]
        public void AttachBehaviourWillEnableBehaviourNode()
        {
            var parent = _container.GetNode<ReactiveNode>();
            var prop = new Property<int>("Prop");
            parent.AttachProperty(prop);
            var triggered = new List<object>();
            parent.Behaviours.Subscribe();

            var behaviour = parent.DescendantPropertyChanged()
                .Where(property => property == prop)
                .Do(p => triggered.Add(new object()))
                .ToUnit();
                        
            prop.Value = 42;
            triggered.Should().HaveCount(0);

            parent.AttachBehaviour(behaviour);
                                            
            prop.Value = 43;
            triggered.Should().HaveCount(1);
        }

        [Test]
        public void DetachBehaviourWillEnableBehaviourNode()
        {
            var parent = _container.GetNode<ReactiveNode>();
            var prop = new Property<int>("Prop");
            parent.AttachProperty(prop);
            var triggered = new List<object>();

            var behaviour = parent.DescendantPropertyChanged()
                .Where(property => property == prop)
                .Do(p => triggered.Add(new object()))
                .ToUnit();

            prop.Value = 42;
            triggered.Should().HaveCount(0);

            parent.AttachBehaviour(behaviour);
            parent.DetachBehaviour(behaviour);

            prop.Value = 43;
            triggered.Should().HaveCount(0);
        }

        [Test]
        public void AttachingBehaviourOnChildDeactivatesIfItIsRemovedFromTreeIfSubscriptionIsDoneInParent()
        {
            var parent = _container.GetNode<ReactiveNode>();
            var child = _container.GetNode<ReactiveNode>();
            parent.ChildNodes.Add(child);
            var prop = new Property<int>("prop");
            child.AttachProperty(prop);
            var triggered = new List<object>();

            var behaviour = child.DescendantPropertyChanged()
                .Where(property => property == prop)
                .Do(p => triggered.Add(new object()))
                .ToUnit();

            parent.Behaviours.Subscribe();

            child.AttachBehaviour(behaviour);

            prop.Value = 43;
            triggered.Should().HaveCount(1);

            parent.ChildNodes.Remove(child);

            prop.Value = 99;
            triggered.Should().HaveCount(1);
        }

        [Test]
        public void AttachingBehaviourOnChildDeactivatesIfItIsRemovedFromTreeIfSubscriptionIsDoneInChild()
        {
            var parent = _container.GetNode<ReactiveNode>();
            var child = _container.GetNode<ReactiveNode>();
            parent.ChildNodes.Add(child);
            var prop = new Property<int>("Prop");
            child.AttachProperty(prop);
            var triggered = new List<object>();

            var behaviour = child.DescendantPropertyChanged()
                .Where(property => property == prop)
                .Do(p => triggered.Add(new object()))
                .ToUnit();

            child.Behaviours.Subscribe();

            child.AttachBehaviour(behaviour);

            prop.Value = 43;
            triggered.Should().HaveCount(1);

            parent.ChildNodes.Remove(child);

            prop.Value = 99;
            triggered.Should().HaveCount(2);
        }

        [Test]
        public void PropertyChangedEventSourceHasNodePath()
        {
            var parent = _container.GetNode<ReactiveNode>();
            var child = _container.GetNode<ReactiveNode>();
            parent.ChildNodes.Add(child);
            var grandChild = _container.GetNode<ReactiveNode>();
            child.ChildNodes.Add(grandChild);
            var prop = new Property<int>("Prop");
            grandChild.AttachProperty(prop);
            var triggered = new List<NodeEvent>();
            parent.Events.Where(e => e.Type == EventType.PropertyChange).Do(triggered.Add).Subscribe();
            prop.Value = 42;

            triggered.Single().Source.Path.Steps.Should().ContainInOrder(new[] {child,grandChild});
        }

        [Test]
        public void NodeAddedEventSourceHasNodePath()
        {
            var parent = _container.GetNode<ReactiveNode>();
            var child = _container.GetNode<ReactiveNode>();
            parent.ChildNodes.Add(child);
            var grandChild = _container.GetNode<ReactiveNode>();
            child.ChildNodes.Add(grandChild);
            var triggered = new List<NodeEvent>();
            parent.Events.Where(e => e.Type == EventType.DescendantAdded).Do(triggered.Add).Subscribe();
            var greatgrandChild = _container.GetNode<ReactiveNode>();
            grandChild.ChildNodes.Add(greatgrandChild);

            triggered.Single().Source.Path.Steps.Should().ContainInOrder(new[] { child, grandChild, greatgrandChild });
        }

        [Test]
        public void NodeRemovedEventSourceHasNodePath()
        {
            var parent = _container.GetNode<ReactiveNode>();
            var child = _container.GetNode<ReactiveNode>();
            parent.ChildNodes.Add(child);
            var grandChild = _container.GetNode<ReactiveNode>();
            child.ChildNodes.Add(grandChild);
            var triggered = new List<NodeEvent>();
            var greatgrandChild = _container.GetNode<ReactiveNode>();
            greatgrandChild.ChildNodes.Add(greatgrandChild);
            parent.Events.Where(e => e.Type == EventType.DescendantRemoved).Do(triggered.Add).Subscribe();
            grandChild.ChildNodes.Remove(greatgrandChild);
            triggered.Single().Source.Path.Steps.Should().ContainInOrder(new[] { child, grandChild, greatgrandChild });
        }

        [Test]
        public void GetDescendantProperties()
        {
            var parent = _container.GetNode<ReactiveNode>();
            var child = _container.GetNode<ReactiveNode>();
            parent.ChildNodes.Add(child);
            var prop = new Property<int>("Prop");
            child.AttachProperty(prop);
            var grandChild = _container.GetNode<ReactiveNode>();
            child.ChildNodes.Add(grandChild);
            var props = new Property<string>("PropS");
            grandChild.AttachProperty(props);

            parent.GetDescendantProperties().Should().BeEquivalentTo(prop, props);
        }

       
    }
}
